mod service_tree;
mod flatpages;
mod ddpages;
mod resolver;
mod session;

use std::{collections::HashSet, path::PathBuf, sync::Arc};

use auth::{oauth2::OAuth2Config, store::AuthStoreConfig};
use axum::{extract::{OriginalUri, State}, http::{header::LOCATION, HeaderMap, StatusCode}, response::Html, routing::get, Router};
use axum_server::tls_rustls::RustlsConfig;
use tera::{Context, Tera};
use tower_http::{
    services::{ServeDir, ServeFile},
    trace::TraceLayer,
};
use tracing::{debug, error};

use ddpages::{DataDrivenPageService, create_data_driven_page_service};
pub use {ddpages::DataDrivenPagesConfig, session::AppSessionConfig};

use crate::{app::service_tree::service_tree, Result};

use self::session::{UserSessionState, AppSessionState};

type HandlerRV = (StatusCode, Html<String>);

struct RenderState {
    tera: Tera,
    e404: HandlerRV,
    dd_pages: DataDrivenPageService,
}

impl RenderState {
    fn new(
        root: &PathBuf,

        datadriven: DataDrivenPagesConfig,
        data_dir: PathBuf,
    ) -> Result<Self> {
        let tera = Tera::new(root.join("templates/**/*.html").to_str().unwrap())?;
        println!("templates={:?}", tera.get_template_names().collect::<Vec<_>>());

        let e404s = tera.render("404.html", &Context::new())
            .unwrap_or_else(|_| raw_error("not found".to_string()));
        let e404 = (StatusCode::NOT_FOUND, Html(e404s));

        let dd_pages = create_data_driven_page_service(datadriven, data_dir)?;

        Ok(Self { tera, e404, dd_pages })
    }

    fn render(&self, template_name: &str, context: &Context) -> HandlerRV {
        match self.tera.render(&template_name, context) {
            Ok(body) => (StatusCode::OK, Html(body)),
            Err(tera::Error { kind: tera::ErrorKind::TemplateNotFound(_), ..}) => self.e404.clone(),
            Err(e) =>  {
                error!(event="template_render_error", error=e.to_string());
                (StatusCode::INTERNAL_SERVER_ERROR, Html(raw_error(e)))
            }
        }
    }
}

#[derive(Clone)]
pub struct AppState {
    root: Arc<String>, 
    render: Arc<RenderState>,
    session: Arc<AppSessionState>,
}

impl AppState {
    fn with_root(self, root: String) -> Self {
        AppState { root: Arc::new(root), ..self }
    }
}

struct InnerApp {
    root: PathBuf,
    datadriven: DataDrivenPagesConfig,
    data_dir: PathBuf,
    app_session_state: AppSessionState,
}

impl InnerApp {

    pub fn into_router(self) -> Result<Router> {
        let render = Arc::new(
            RenderState::new(&self.root, self.datadriven, self.data_dir)?
        );
        let template_names: HashSet<&str> = render.tera.get_template_names().collect();
        let service_tree = service_tree(template_names, "index.html");
        debug!(service_tree=?service_tree);
        let session = Arc::new(self.app_session_state);

        let state = AppState { 
            root: Arc::new("".to_owned()), 
            render: render.clone(),
            session: session.clone(),
        };

        let app = Router::new().fallback(get(r404));

        let app = service_tree.into_iter().fold(
            app, 
            |r: Router<AppState>, sd| {
                let path_nots = sd.root_as_path_nots();
                let r = r.nest(
                    &sd.root_as_path(), 
                    if render.dd_pages.has_service(&sd.root_as_str()) {
                        render.dd_pages.create_router(state.clone(), sd)
                    } else {
                        flatpages::service_router(state.clone(), sd)
                    }                
                );
                // add route on no-trailing-slash variant to 301 redirect (same behavior as nginx on a dir)
                let r = match path_nots.as_ref() {
                    Some(path) => r.route(path, get(redir_to_folder)),
                    None => r,
                };
                r
            }
        );

        let app = app
            .route("/login", get(session::login_handler))
            .route("/token", get(session::redirect_handler))
            .with_state(state)
            .layer(axum::middleware::from_fn_with_state(session, session::session_manager))
            ;

        // note that these routes are not getting state management, as they are added after state management layer
        let app = app
            .nest_service("/assets", ServeDir::new(self.root.join("assets")))
            .nest_service("/images", ServeDir::new(self.root.join("images")))
            .nest_service("/favicon.ico", ServeFile::new(self.root.join("favicon.ico")))
            .layer(TraceLayer::new_for_http())
            ;

        Ok(app)
    }

}

async fn r404(
    State(AppState { render, ..}): State<AppState>
) -> HandlerRV {
    debug!(message="r404_activation");
    render.e404.clone()
}

async fn redir_to_folder(
    OriginalUri(uri): OriginalUri,
) -> (StatusCode, HeaderMap, Html<String>) {
    let path = uri.path();
    let mut m = HeaderMap::new();

    if path.ends_with('/') {
        (StatusCode::INTERNAL_SERVER_ERROR, m, Html(raw_error("[redir_to_folder] source is already folder")))
    } else if let Some(w) = (path.to_string() + "/").parse().ok() { 
        m.insert(LOCATION, w);
        (StatusCode::MOVED_PERMANENTLY, m, Html("".to_string()))
    } else {
        (StatusCode::INTERNAL_SERVER_ERROR, m, Html(raw_error("[redir_to_folder] unable to parse target uri")))
    }
}

pub struct App {
    inner_app: InnerApp,
    bind_addr: String,
    tls_config: Option<RustlsConfig>,
}

impl App {
    pub fn new(
        root: PathBuf, 
        pagedata: toml::Value, 
        datadriven: DataDrivenPagesConfig, 
        bind_addr: String, 
        tls_config: Option<RustlsConfig>,
        app_session_config: AppSessionConfig,
        oauth2_config: OAuth2Config,
        auth_store_config: AuthStoreConfig,
    ) -> Self {
        let data_dir = root.join("data");
        let app_session_state = AppSessionState::new(
            app_session_config, 
            oauth2_config, 
            auth_store_config, 
            pagedata
        );
        Self { 
            inner_app: InnerApp { root, datadriven, data_dir, app_session_state }, 
            bind_addr, 
            tls_config 
        }
    }

    pub async fn run(self) {
        if let Some(tls_config) = self.tls_config {
            axum_server::bind_rustls(self.bind_addr.parse().unwrap(), tls_config)
                .serve(self.inner_app.into_router().unwrap().into_make_service())
                .await
                .unwrap();
        } else {
            axum_server::bind(self.bind_addr.parse().unwrap())
                .serve(self.inner_app.into_router().unwrap().into_make_service())
                .await
                .unwrap();
        }
    }
}


fn raw_error<T: ToString>(e: T) -> String {
    format!(r##"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Error!</title>
</head>
<body>
    <h1>Error: {e}</h1>
</body>
</html>
"##, e=tera::escape_html(&e.to_string())
    )
}
